8W - 아르고 워크플로우

개요

이번 주차는 CICD를 다룬다.
최종 실습으로는 풀 CICD를 진행할 것인데, 이를 위해 먼저 사용할 툴들에 대한 실습 내용들을 문서로 작성한다.
로컬에서 실습을 진행하다가 eks로 넘어갔기 때문에 중간 중간 사진 흐름이 어색할 수도 있다.

사전 지식

CICD에 대해서는 따로 정리하지 않는다.
너무 잘 알려진 개념이라 기초 지식이라 생각하기도 했고, 실질적으로 CICD를 생각하는데 가장 중요한 것은 조직 문화와 협업 방식에 따라 어떻게 전략을 취하는가이다.
여타 다른 영역의 지식들과 다르게 CICD의 개념 자체는 어려운 게 하나도 없기에, 어떤 식으로 전략을 짜는 것이 좋은지 간단하게 실습을 하면서 익히는 시간을 가져보도록 한다.

Argo Workflow란

image.png
쿠버네티스에서 워크플로우을 만들 수 있는 툴.
워크플로우를 CRD로 구현하여 관리할 수 있게 해준다.
다음의 특징을 가지고 있다.

머신러닝 클러스터를 운영하거나, 대규모 데이터 배치 처리가 필요할 때 많이 쓰인다.
또 CICD를 할 때 활용하기도 한다.
이밖에도 워크플로우로 단계적으로 무언가를 적용해야할 때 유용하게 쓰인다.

워크플로우 흐름

일단 워크플로우라는 것이 정확하게 어떤 것인지를 알 필요가 있다.

워크플로우는 초록 블록처럼, 여러 단계에 걸쳐서 특정 기능들을 수행하는 파드들을 실행하는 것이다.
젠킨스를 쓴다면 각 스테이지 별로 행동을 정의하고 관련한 코드를 작성한다.
마찬가지로 워크플로우도 그런 식으로 양식을 작성하면, 각 스텝이 파드로서 실행되는 것이다.

그래서 워크플로우에 대해서 기본적으로는 구조랄 것도 그다지 없다.[1]
워크플로우를 만들고 관리해주는 컨트롤러, 그리고 이 컨트롤러를 조금 편리하게 조작할 수 있게 해주는 아르고 서버가 있을 뿐이다.
그냥 말 그대로 워크플로우따라 파드가 만들어지는데, 사용자가 이걸 CRD로 클러스터에 상태를 등록하면 컨트롤러가 이 정보를 받아 워크플로우를 진행한다.

워크플로우 컨트롤러는 이런 식으로 동작한다.
파드 상태, 워크플로우 리소스 상태 살피면서 큐 관리하고, 큐에 맞춰서 워크플로우를 실행해주는 방식.

전체 구조와 기능


하는 일만 따지면 구조는 간단하지만, 이를 위해 들어가는 추가 기능들을 합치면 조금 더 복잡해진다.
실습을 하고 새삼 다시 보니 위 그림이 참 요약이 잘 된 그림이라는 생각이 드는데, 일단 워크플로우가 돌아가는 방식은 위에서 설명했으니 이 워크플로우를 만들거나 트리거하는 방법을 기준으로 흐름을 설명해보겠다.
위에서 워크플로우는 클러스터 리소스로서, 워크플로우 컨트롤러가 이 정보를 확인하고 실제 워크플로우를 진행한다는 것은 변함이 없다.
이때 워크플로우 리소스를 만드는 방식은 다음과 같이 다양하다.

기본적으로 사용자의 요청을 처리하는 아르고 서버가 존재하기 때문에, 이 서버를 이용해주는 것이 흔한 이용 방식이다.
특히 이 서버를 거치면 시각화도 이쁘게 되는 것을 넘어 직접적으로 리소스를 등록하는 것보다 상태 업데이트도 깔끔하게 이뤄지기 때문에, 웬만해서는 그냥 이렇게 사용하는 것을 추천한다.

워크플로우는 CRD이기 때문에 많은 워크플로우가 진행되다 보면 정보가 많이 쌓여 Etcd에 부하가 쌓인다.
그래서 이것을 막기 위해 워크플로우의 정보들을 아카이빙 하는 것이 가능한데, 현재는 RDBMS를 활용할수 있다.

또한 워크플로우의 각 단계는 파드로 만들어지기 때문에 기본적으로 서로 데이터를 공유하는 것이 제한된다.
양식 작성법을 보면 간단한 데이터는 전달할 수 있다는 걸 보게 될 것이다.
그러나 머신러닝을 하거나 이미지 빌드 등의 작업을 수행한다고 생각해보자.
이러면 전달할 데이터의 크기가 어마무시하게 클 수도 있다.
이럴 때를 위해, 아르고 워크플로우는 아티팩트(artifact)라는 개념으로 외부 저장소를 두어 데이터 공유나 통합, 처리를 용이하게 할 수 있도록 돕는다.
아티팩트의 종류에는 S3와 같은 오브젝트 스토리지, 깃 등이 가능하다.

워크플로우(Workflow) 양식 작성법

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
spec:
  entrypoint: hello-world       # 템플릿 진입점
  arguments:
    parameters:
    - name: message
      value: hello world
  templates:
  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["{{inputs.parameters.message}}"] 

아르고 워크 플로우의 핵심은 말 그대로 워크플로우 리소스니, 이 놈을 먼저 보고 이후 다양한 설정들을 다루겠다.
기본적으로 위 모습처럼 구성을 하는데, 일단 generateName을 이용해 여러 워크플로우가 생성될 때 접두사가 붙도록 설정한다.
스펙에서는 템플릿을 여러 개 작성한 후, 최종적으로 이 워크플로우의 진입점이 되는 템플릿을 entrypoint로 명시해주면 끝이다!
image.png
너무 간단하게 핵심을 문서에서 잘 정리해서 가져왔다.[2]
결국 워크플로우 양식의 핵심은 템플릿을 여러 개 원하는 대로 작성하고, 진입점만 잘 박아주는 것이다.

일단 워크플로우 오브젝트는 크게 두 가지 기능을 한다는 것을 알아두자.

템플릿 유형

워크플로우에서 템플릿은 곧 각 기능 단위, 즉 함수를 의미한다.
템플릿은 총 9개의 유형으로 분화된다.

container

  - name: hello-world
    container:
      image: busybox
      command: [echo]
      args: ["hello world"]

그냥 일반 컨테이너를 의미하며, 흔히 파드 스펙에 작성하는 컨테이너 스펙을 그대로 작성하면 된다.
이렇게 실행하면 기본적으로 하나의 컨테이너가 담긴 하나의 파드가 만들어진다.

script

  - name: gen-random-int
    script:
      image: python:alpine3.6
      command: [python]
      source: |
        import random
        i = random.randint(1, 100)
        print(i)

컨테이너 중 그냥 인자를 넣어서 실행하는 역할을 하는 템플릿은 아예 이렇게 스크립트 유형으로 만들어주면 된다.
source필드를 통해 편하게 스크립트를 작성할 수 있게 해두었다.

resource

  - name: k8s-owner-reference
    resource:
      action: create
      manifest: |
        apiVersion: v1
        kind: ConfigMap
        metadata:
          generateName: owned-eg-
        data:
          some: value

쿠버네티스의 리소스를 조작하는 템플릿이다!
get, create, list, delete 등의 다양한 작업을 할 수 있다.
워크플로우가 지워지면 해당 리소스도 같이 지워지도록 돼있다.
소유자 참조 필드를 지워주면 독립적으로 성립하게 할 수도 있다.
당연히 이게 제대로 이뤄지려면 이 워크플로우를 실행하는 컨트롤러가 관련한 권한을 가지고 있어야만 한다.

suspend

  - name: delay
    suspend:
      duration: "20s"

그냥 잠시 대기하는 템플릿 유형이다.
duration 필드 없이 그냥 쓰면 관리자가 직접 resume을 시켜줘야 한다.

plugin

  - name: main
    plugin:
      slack:
        text: "{{workflow.name}} finished!"

외부의 플러그인 조작을 하는 템플릿이다.

containerSet

    - name: main
      volumes:
        - name: workspace
          emptyDir: { }
      containerSet:
        volumeMounts:
          - mountPath: /workspace
            name: workspace
        containers:
          - name: a
            image: argoproj/argosay:v2
            command: [sh, -c]
            args: ["echo 'a: hello world' >> /workspace/message"]
          - name: b
            image: argoproj/argosay:v2
            command: [sh, -c]
            args: ["echo 'b: hello world' >> /workspace/message"]
          - name: main
            image: argoproj/argosay:v2
            command: [sh, -c]
            args: ["echo 'main: hello world' >> /workspace/message"]
            dependencies:
              - a
              - b

한 파드에 여러 컨테이너를 실행할 때는 containerSet을 사용한다.
아무래도 pause 컨테이너가 더 만들어지지 않으니 리소스 낭비는 적다.
그리고 emptyDir 볼륨을 이용해 데이터를 공유할 수 있다는 것도 장점.
재밌는 것은 여기에 dependencies를 설정해서 이 컨테이너 간 의존성도 정할 수 있다는 것이다.
image.png
주의할 점은 이것이다.
자원 요청 시에 모든 컨테이너의 합산으로 요청이 되기 때문에, 최대 2기가가 필요한 필요하다면 컨테이너의 요청량 합이 2기가가 되도록 세팅해야 낭비가 없다.

http

    - name: http
      inputs:
        parameters:
          - name: url
      http:
        timeoutSeconds: 20 # Default 30
        url: "{{inputs.parameters.url}}"
        method: "GET" # Default GET
        headers:
          - name: "x-header-name"
            value: "test-value"
        body: "test body" # 보낼 요청 바디
        # 성공 조건 명시
        # 사용가능한 변수는 다음과 같다.
        #  request.body: string, the request body
        #  request.headers: map[string][]string, the request headers
        #  response.url: string, the request url
        #  response.method: string, the request method
        #  response.statusCode: int, the response status code
        #  response.body: string, the response body
        #  response.headers: map[string][]string, the response headers
        successCondition: "response.body contains \"google\"" # available since v3.3

말그대로 http 요청을 날리는 유형이다.
성공 조건을 명시하는 것도 가능하다.
경험 상 이 템플릿은 생각보다 실행이 조금 불안정해서, 차라리 container 템플릿으로 직접 요청을 날리는 식으로 쓰는 게 더 좋은 것 같다.

템플릿 주입기

아래 두 템플릿 유형은 조금 특별한데, 다른 템플릿들을 명시하여 순서나 그래프를 지정하는 역할을 하는 템플릿이다.
이 아래 템플릿들을 통해, 위 템플릿들의 순서나 구조를 짜맞춰가며 진정한 워크플로우를 만들 수 있는 것이다!

steps
spec:
  entrypoint: hello-hello-hello
  templates:
  - name: hello-hello-hello
    단계 지정
    steps:
	# 첫 단계
    - - name: hello1
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "hello1"
    # 두번째 단계
    - - name: hello2a
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "hello2a"
      # 두번째 단계가 병렬적으로 실행!
      - name: hello2b
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "hello2b"

  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["{{inputs.parameters.message}}"]

steps는 단계 별로 템플릿 실행 단계를 정의하는 방식이다.
보다시피 steps는 이중 리스트 구조로 되어 있으며, 상위 리스트는 템플릿 간 순서를 구분짓고, 하위 리스트는 병렬적으로 실행할 템플릿을 넣는 공간이 된다.
image.png
위 워크플로우가 실행되면 이런 식의 모양이 될 것이다.
트리 구조처럼 자식 노드의 부모가 하나로만 구성되는 워크플로우를 짤 때 직관적으로 짤 수 있어서 좋다.

dag
  - name: diamond
    dag:
      tasks:
      - name: A
        template: echo
        arguments:
          parameters: [{name: message, value: A}]
      - name: B
        dependencies: [A]
        template: echo
        arguments:
          parameters: [{name: message, value: B}]
      - name: C
        dependencies: [A]
        template: echo
        arguments:
          parameters: [{name: message, value: C}]
      - name: D
        dependencies: [B, C]
        template: echo
        arguments:
          parameters: [{name: message, value: D}]

스텝이 아니라 DAG(Directed Acyclic Graph, 방향이 있는 비순환 그래프)를 이용하는 방법도 있다.
이것은 직접저으로 dependencies로 의존성을 표현해주기에, 자식 노드가 여러 부모를 가지는 것도 가능하다.
또한 더 복잡한 구조를 만들기에 용이하나, 막상 해보면 이 녀석으로 양식 작성할 때 조금 귀찮은 측면도 없잖아 있기는 하다.
아무튼 각각을 dag.tasks에 리스트로 이름을 작성한다.
위 워크플로우는 A -> B,C -> D와 같은 식으로 실행될 것이다.

이런 주입 유형의 템플릿들에 대해서는 [[#반복문, 조건문]]을 걸어서 더 다양하게 워크플로우를 짤 수 있다.

데이터 입출력, 인자(arguements)

템플릿 각각이 하나의 함수라고 했는데, 실제 함수가 그러하듯 템플릿은 입력과 출력을 하는 것이 가능하다.
어떤 식으로 설정을 하는지를 알기 이전에, 인자에는 두 가지 유형이 있다는 것을 먼저 짚고 가야 한다.
arguement의 유형에는 parameters, artifacts 두 가지가 있다.
파라미터는 말 그대로 워크플로우 내에서 사용할 인수들을 넣을 때 사용한다.
작은 데이터를 간단하게 주고 받을 때, 함수 입출력처럼 흔히 사용되는 방식이다.
아티팩트는 외부 저장 공간에서 데이터를 가져올 때 사용한다.
크거나 영속성을 가져야하는 데이터가 있을 때 주로 사용된다.

      artifacts:
      # 깃 메인브랜치 가져와서 /src에 위치시킴
      # 리비전은 브랜치 이름이던 커밋이름이던, 태그던 다 가능
      - name: argo-source
        path: /src
        git:
          repo: https://github.com/argoproj/argo-workflows.git
          revision: "main"
      - name: kubectl
        path: /bin/kubectl
        mode: 0755
        http:
          url: https://storage.googleapis.com/kubernetes-release/release/v1.8.0/bin/linux/amd64/kubectl
      # s3의 데이터를 받아와서 저장
      - name: objects
        path: /s3
        s3:
          endpoint: storage.googleapis.com
          bucket: my-bucket-name
          key: path/in/bucket
          accessKeySecret:
            name: my-s3-credentials
            key: accessKey
          secretKeySecret:
            name: my-s3-credentials
            key: secretKey

이런 식으로 깃이나 S3 등 다양한 곳에서 데이터를 가져와서 템플릿 실행 환경에 넣어줄 수 있다.
참고로 아티팩트라고 해서 무조건 외부 저장소를 사용해야 한다는 뜻은 아니다!
파라미터 쓰듯이 아티팩트를 쓰는 것도 가능하긴 한데, 가급적이면 목적에 맞게 구분해서 쓰는 것이 당연히 좋겠지?

apiVersion: v1
kind: ConfigMap
metadata:
  name: artifact-repositories # 이 이름을 지으면 모든 워크플로우가 이 컨맵의 데이터를 아티팩트로 활용할 수 있다.
  annotations:
    # 이 어노테이션의 값으로 지정된 데이터는 아무론 설정없이 모든 워크플로우가 쓸 수 있다.
    # 다른 데이터들은 각 워크플로우에서 추가 설정을 하긴 해야 한다.
    workflows.argoproj.io/default-artifact-repository: default-v1-s3-artifact-repository
data:
  default-v1-s3-artifact-repository: |
    s3:
      bucket: my-bucket
      endpoint: minio:9000
      insecure: true
      accessKeySecret:
        name: my-minio-cred
        key: accesskey
      secretKeySecret:
        name: my-minio-cred
        key: secretkey
  v2-s3-artifact-repository: |
    s3:
      ...

아티팩트를 여러 개 쓰게 되면 일일히 워크플로우에 쓰기 힘든데, 이런 식으로 컨피그맵으로 관리하는 것도 가능하다.
위처럼 여러 데이터를 넣는 것도 가능한데, 이때 기본으로 쓰일 데이터를 지정하고 싶다면 어노테이션을 저리 달자.

spec:
  artifactRepositoryRef:
    configMap: my-artifact-repository
    key: v2-s3-artifact-repository 

기본이 아닌 데이터들은 이런 식으로 워크플로우에서 이런 식으로 필드를 주면 된다.
해당 컨피그맵에서 한 키를 지정할 때 이렇게 필드를 명시한다.

이제 인자의 유형과 설정 방법을 간단하게 봤으니, 실제로 인자를 템플릿의 입출력에 어떻게 이용하는지 보자.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: example-
spec:
  entrypoint: main
  arguments:
    parameters:
    - name: workflow-param-1
  templates:
  - name: main
    dag:
      tasks:
      - name: step-A 
        template: step-template-a
        arguments:
          parameters:
          - name: template-param-1
            value: "{{workflow.parameters.workflow-param-1}}"

  - name: step-template-a
    inputs:
      parameters:
        - name: template-param-1
    script:
      image: alpine
      command: [/bin/sh]
      source: |
          echo "{{inputs.parameters.template-param-1}}"
	outputs:
	  parameters:
		- name: output-param-1
		  valueFrom:
			path: /p1.txt
	  artifacts:
		- name: output-artifact-1
		  path: /some-directory

각 템플릿의 입력과 출력은 기본적으로 inputs, outputs 필드이다.
템플릿 내에서 어떤 입력을 활용할 때는 inputs 필드를 정의하고 여기에 들어오는 데이터를 사용하면 된다.
출력하고 싶은 값은 outputs 필드에 작성해서 넣어주면 된다.

그렇다면 각 템플릿은 inputs 필드를 이용해 데이터를 받는데 실제로 해당 필드로 데이터를 넣어주는 통로가 무엇이냐?
그것이 바로 arguements 필드이다.
위 예시에서 dag 템플릿은 하위 템플릿에 대해 arguements를 지정하고 있는 것이 보인다.
이것이 바로 하위 템플릿에 어떠한 데이터를 넣겠다, 하는 뜻이다.
위 예시에는 워크플로우 단위에서 arguements로 사용할 인자를 정의하는 것도 담겨있다.

    arguments:
      parameters:
      - name: template-param-2
        value: "{{tasks.step-A.outputs.parameters.output-param-1}}"
      artifacts:
      - name: input-artifact-1
        from: "{{tasks.step-A.outputs.artifacts.output-artifact-1}}"

outputs 필드의 데이터를 arguements로 넣을 때는 이런 식으로 지정한다.
그럼 다음 템플릿에서는 이런 식으로 출력 데이터를 받아서 사용할 수 있을 것이다.
주의할 게 있는데, 파라미터로 들어왔을 때는 value라는 필드를 쓰지만 아티팩트로 들어올 때는 from을 써야한다는 것이다.
(헷갈리게시리)
위의 예시에서 arguements.parameters라고 하여 파라미터라는 필드를 추가 사용하는 것이 보일 것이다.

    - - name: generate
        template: gen-random-int-bash
    - - name: print
        template: print-message
        arguments:
          parameters:
          - name: message
            value: "{{steps.generate.outputs.result}}"  # The result of the here-script

참고로 http, container, script 유형의 템플릿은 우리가 명시해주지 않아도 자체적으로 result라는 유형의 출력값을 가진다.
gen-random-int-bash 템플릿은 script 유형인데, 덕분에 다음 템플릿에서 ouputs.result라고 해서 데이터를 받아올 수 있다.

반복문, 조건문

  - name: loop-sequence-example
    steps:
    - - name: hello-world-x5
        template: hello-world
        withSequence:
          count: "5"
- - name: test-linux
	template: cat-os-release
	arguments:
	  parameters:
	  - name: image
		value: "{{item.image}}"
	  - name: tag
		value: "{{item.tag}}"
	withItems:
	- { image: 'debian', tag: '9.1' }       
	- { image: 'debian', tag: '8.9' }      
	- { image: 'alpine', tag: '3.6' }     
	- { image: 'ubuntu', tag: '17.10'}   
- - name: test-linux
	template: cat-os-release
	arguments:
	  parameters:
	  - name: image
		value: "{{item.image}}"
	  - name: tag
		value: "{{item.tag}}"
	withParam: "{{inputs.parameters.os-list}}"    

with로 시작하는 필드를 넣어 반복적으로 템플릿을 실행할 수 있다.
반복문은 총 3가지 방식으로 사용할 수 있다.

withParams를 이용하면 동적으로 워크플로우의 횟수를 지정할 수 있게 되기 때문에 매우 유용하다!
이렇게 반복되는 템플릿으로부터 출력물을 받아올 때는 데이터가 리스트 형식으로 들어온다.
일종의 aggregate된 결과를 볼 수 있게 되는 것이다.

- name: coinflip
  steps:
    # 첫번째 코인
    - - name: flip-coin
        template: flip-coin
	# 이 둘중 하나만 실행된다.
    - - name: heads
        template: heads
        when: "{{steps.flip-coin.outputs.result}} == heads"
      - name: tails
        template: tails
        when: "{{steps.flip-coin.outputs.result}} == tails"
	# 두번째 코인
	- - name: flip-again
        template: flip-coin
	# 굳이 조건문 복잡하게 다는 것도 지원!
	- - name: complex-condition
        template: heads-tails-or-twice-tails
        when: >-
            ( {{steps.flip-coin.outputs.result}} == heads &&
              {{steps.flip-again.outputs.result}} == tails
            ) ||
            ( {{steps.flip-coin.outputs.result}} == tails &&
              {{steps.flip-again.outputs.result}} == tails )
      - name: heads-regex
        template: heads 
        when: "{{steps.flip-again.outputs.result}} =~ hea"
      - name: tails-regex
        template: tails 
        when: "{{steps.flip-again.outputs.result}} =~ tai"

이런 식으로 when 필드를 써주면 조건문이 된다.
이로써 워크플로우를 분기시켜서 실행하는 것도 가능해진다.

spec:
  entrypoint: coinflip
  templates:
  - name: coinflip
    steps:
    - - name: flip-coin
        template: flip-coin
    - - name: heads
        template: heads 
        when: "{{steps.flip-coin.outputs.result}} == heads"
      - name: tails
        template: coinflip # 이 step 템플릿의 이름이다!
        when: "{{steps.flip-coin.outputs.result}} == tails"

조건문이 있다면.. 종료 조건처럼 쓸 수도 있다는 뜻이고.. 그렇다면 재귀 템플릿도 가능하다..!
위의 워크플로우는 동전이 앞면이 나올 때까지 계속 재귀를 돌 것이다.
image.png
이런 식으로 말이다.

volumes, volumeClaimTemplates

spec:
  entrypoint: print-secrets
  volumes:
  - name: my-secret-vol
    secret:
      secretName: my-secret
  templates:
  - name: print-secrets
    container:
      image: alpine:3.7
      command: [sh, -c]
      args: ['
      ']
      env:
      - name: MYSECRETPASSWORD 
        valueFrom:
          secretKeyRef:
            name: my-secret 
            key: mypassword
      volumeMounts:
      - name: my-secret-vol 
        mountPath: "/secret/mountpath"
---
spec:
  entrypoint: volumes-pvc-example
  volumeClaimTemplates:                 # define volume, same syntax as k8s Pod spec
  - metadata:
      name: workdir                     # name of volume claim
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi      

템플릿 간 파일 공유, 혹은 영구 스토리지 참조 등이 필요하다면 이렇게 쿠버네티스 볼륨을 그대로 사용해줄 수 있다!
얼마나 편리한가!
스테이트풀셋이 그러듯이 PVC 템플릿 넣는 것도 쌉가능이당
근데 주의할 점이 있는데, 이건 위에서 말한 아티팩트와는 다르다.
이 볼륨은 그저 워크플로우를 실행하는데 있어 공통적으로 사용하기 위한 공간에 불과하다.
워크플로우의 각 단계는 파드이기에 기본적으로 서로 데이터를 공유하는 것이 어렵기 때문에 arguement에 parameter를 사용하는 건데, 이걸로도 부족하다면 이런 식으로 공용 공간을 두어 사용하는 게 효과적일 것이다.
아티팩트로도 대체가 가능하긴 한데, 역시나 어떻게 이용할지는 사용자의 몫이라 할 수 있겠다.

실행 주체

한 워크플로우는 하나의 서비스 어카운트를 가지고 실행된다.
이건 .spec.serviceAccountName 필드를 작성해주면 되는데, 명시 안 하면 해당 네임스페이스의 default 서아로 실행된다.
다른 리소스 참조라던가 하는 행위를 하기 위해서는 서아의 RBAC 세팅이 필수인데 default 사용하는 건.. 당연히 안 좋겠지..?

워크플로우가 실행되는 동안 몇 가지 훅을 거는 게 가능하다.

spec:
 entrypoint: main
 hooks:
   exit:
     template: http
   running:
     expression: workflow.status == "Running"
     template: http

위의 예시는 워크플로우가 종료됐을 때 http라는 템플릿을 실행한다.
그리고 실행 중일 때는 워크플로우의 status가 Running인 경우에도 http 템플릿을 실행한다.
workflow.status == "Succeeded"일 때 요청을 보내게 한다던가 하는 것도 가능할 것이다.

spec:
 entrypoint: main
 onExit: http

참고로 종료에 대한 훅은 이렇게도 가능하다.

재시작

    retryStrategy:
      limit: 10
      retryPolicy: "Always"
      backoff:
        duration: "1s"   
        factor: 2
        maxDuration: "1m" 
      affinity:
        nodeAntiAffinity: {}

템플릿이나 워크플로우 단위로 재시작 전략을 설정할 수 있다.
backoff 필드는 실패해서 재시작해야 할 때 어느 정도의 시간을 두고 재시작할지를 나타낸다.
affnity 필드는 실패한 노드에서 다시 실행되지 않도록 하기 위한 필드인데, 현재 버전에서는 여기 추가 커스텀은 안 되고 딱 저렇게만 사용가능하다고 한다.

기타 리소스 양식 작성법

WorkFlowTemplate, ClusterWorkFlowTemplate

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: hello-world-template-global-arg
spec:
  templates:
    - name: hello-world
      container:
        image: busybox
        command: [echo]
        args: ["{{workflow.parameters.global-parameter}}"]
---
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-wf-global-arg-
spec:
  entrypoint: print-message
  arguments:
    parameters:
      - name: global-parameter
        value: hello
  templates:
    - name: print-message
      steps:
        - - name: hello-world
            templateRef:
              name: hello-world-template-global-arg
              template: hello-world

기본적으로 워크플로우는 만들어지면 바로 동작을 시작한다.
근데 보통은 워크플로우 템플릿을 짜두고 필요할 때마다 이에 맞춰서 이용하지 않는가?
그럴 때 사용하는 것이 바로 이 템플릿 리소스다.
아래 워크플로우는 스텝 템플릿 안에 templateRef로 만들어둔 템플릿을 가져오는 것이 보인다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: workflow-template-hello-world-
spec:
  workflowTemplateRef:
    name: workflow-template-submittable

워크플로우템플릿 리소스에 진입점 같은 세팅을 해뒀다면, 아예 이렇게 workflowTemplateRef를 이용해 통째로 이용하는 것도 가능하다.

  workflowTemplateRef:
    name: cluster-workflow-template-submittable
    clusterScope: true

ClusterWorkflowTemplate이라고 kind를 지정해 클러스터 전역으로 만들 수도 있는데, 그걸 사용할 때는 위처럼 clusterScope: true를 명시해야 한다.

CronWorkflow

apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: test-cron-wf
spec:
  schedules:
    - "* * * * *"
  concurrencyPolicy: "Replace"
  startingDeadlineSeconds: 0
  workflowSpec:
    entrypoint: date
    templates:
    - name: date
      container:
        image: alpine:3.6
        command: [sh, -c]
        args: ["date; sleep 90"]

말 그대로 크론으로 하는 방식!
크론잡이란 비슷한 느낌이다.

트리거

실질적으로 워크플로우를 사용할 때, 직접 워크플로우 가동해야겠다 하고 하는 사용 케이스는 많이 없을 것이다.
주기적으로 데이터를 학습시켜야 한다는 특정 조건, 아니면 개발을 마쳐서 깃에 머지가 된 시점에 워크플로우가 실행되길 바라는 게 대부분일 것이다.
이럴 때 웹훅 트리거, 혹은 api 요청을 보내서 워크플로우를 실행하는 것이 가능하다.[3]
이벤트라는 api 엔드포인트를 노출하고 있으며, 이쪽으로 트리거링을 할 수 있다.
참고로 문서에서도 언급하는데 이건 Argo Events와는 무관하게 존재하는 워크플로우의 리소스이다.
물론 아르고 이벤트와도 연동하여 이용할 수도 있긴 하다.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: my-wf-tmple
  namespace: argo
spec:
  templates:
    - name: main
      inputs:
        parameters:
          - name: message
            value: "{{workflow.parameters.message}}"
      container:
        image: busybox
        command: [echo]
        args: ["{{inputs.parameters.message}}"]
  entrypoint: main
---
apiVersion: argoproj.io/v1alpha1
kind: WorkflowEventBinding
metadata:
  name: event-consumer
spec:
  event:
    # 들어온 요청 중 어떤 것이 이벤트인지 정하는 필드.
    # discriminator는 아래 참조.
    selector: payload.message != "" && metadata["x-argo-e2e"] == ["true"] && discriminator == "my-discriminator"
  submit:
    workflowTemplateRef:
      name: my-wf-tmple
    arguments:
      parameters:
      - name: message
        valueFrom:
          event: payload.message

이런 식으로 워크플로우템플릿, 그리고 이벤트바인딩 리소스를 만들어야 한다.
이 워크플로우 이벤트 바인딩 리소스가 api를 노출하는 핵심으로, 이 리소스를 만들면 다음의 경로로 api가 노출된다.

아르고서버/api/v1/events/네임스페이스/식별자(discriminator)

뒤 식별자는 단순히 검증용이라 필수적으로 넣어야 한다 이런 건 아니다.

curl $ARGO_SERVER/api/v1/events/argo/my-discriminator \
    -H "Authorization: $ARGO_TOKEN" \
    -H "X-Argo-E2E: true" \
    -d '{"message": "hello events"}'

위의 리소스라면 요청을 이렇게 보내면 트리거될 것이다.

근데 보통 웹훅을 보내는 서비스들은 저마다 각자의 웹훅 쏘는 방식이 정해져있다..
그래서 api로 보내는 웹훅에 대해서 추가적인 설정을 할 수 있다.[4]
이런 추가 설정이 필요한 이유는 궁극적으로는 인증 인가 때문인데, 흐름을 정리하자면 다음과 같다.

위 문서 링크의 세팅 내용들은 이 흐름을 위한 각종 리소스들을 설정하는 과정이다.
좀 자세하게 좀 설명 좀 해주지 좀

보안

image.png
아르고 워크플로우가 자체적으로 api 서버를 가지고 있는 만큼, 이 api를 사용할 수 있는 유저에 대한 신원을 확인하는 절차가 마련되어 있다.
다만 자체적으로 유저의 신원을 관리하는 것은 아니고, 전적으로 외부(아르고 입장에서)에 인증을 위임한다.
크게는 세 가지 방식이 존재한다.

실습 진행

이제 본격적으로 아르고 워크플로우를 사용해보자.

테라폼 세팅

###############################################################################
#### Argo Workflow
###############################################################################
resource "helm_release" "argo_workflows" {
  repository = "https://argoproj.github.io/argo-helm"
  chart = "argo-workflows"
  version = "0.45.11"
  name = "argo-workflows"
  namespace = "argo"

  create_namespace = true
  values = [
    <<-EOF
    crds:
      install: true
      keep: true
    createAggregateRoles: true
    singleNamespace: false
    workflow:
      serviceAccount:
        create: true
        name: "argo-workflow"
      rbac:
        create: true
        agentPermissions: true

    controller:
      rbac:
        create: true
      configMap:
        create: true
      serviceAccount:
        create: true
      workflowNamespaces:
        - default

    server:
      enabled: true
      authModes: 
      - client
      ingress:
        enabled: true
    EOF
  ]
}

전반적으로는 이렇게 테라폼으로 헬름을 이용해 설치를 진행했다.
다양한 네임스페이스에서 쓸 수 있도록, 또 해당 네임스페이스에 기본적으로 워크플로우가 돌아갈 때 사용될 서비스 어카운트가 생기도록 세팅을 했다.
참고로 아르고 시리즈는 전부 argo 네임스페이스에 만드는 게 권장되는데, 다른 네임스페이스에 둘 거라면 커스텀 세팅을 더 해야 한다.
image.png
워크플로우는 간단하게 성공.
그런데 로그인을 하려면 외부의 인증을 받아야만 한다.
위에서 말했듯, 워크플로우가 자체적으로 유저의 신원을 관리하지 않기 때문이다.
SSO를 이용하여 완전히 OAuth를 제공해주는 업체에 인증을 맡겨도 되는데, kubectl로 관리가 가능한 만큼 당연히 클러스터의 신원을 이용하는 것도 가능하다.
image.png
argo-workflows-server가 기본적으로 필요한 권한을 가졌으니, 따로 뭐 안 만들고 이걸 이용해도 될 것이다.

k create token -n argo argo-workflows-server

....
image.png
넣을 때.. Bearer를 앞에 꼭 같이 넣어라..
image.png
아무튼 이렇게 볼 수 있게 된다.

간단 워크플로우 생성

웹 ui는 확인용으로만 쓸 거고, 간단한 워크플로우를 한번 만들어본다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
spec:
  serviceAccountName: argo-workflow
  entrypoint: print-message
  arguments:
    parameters:
    - name: message
      value: hello world
  templates:
  - name: print-message
    inputs:
      parameters:
      - name: message
    container:
      image: busybox
      command: [echo]
      args: ["{{inputs.parameters.message}}"] 

generateName 필드로 이름이 만들어지는 방식이라 이 양식은 apply할 수 없고, 무조건 create를 해야 한다.
image.png
엔트리포인트를 한번 잘못 잡아서 에러가 났다.
image.png
네임스페이스에서 워크플로우가 실행되는데 default 서비스 어카운트가 사용되면 문제가 발생하니 웬만해서는 꼭 serviceAccountName 필드를 세팅하자.
image.png
아무튼 이렇게 하면 제대로 실행이 되는 것을 확인할 수 있다.
image.png
워크플로우는 전부 잡처럼 실행되기 때문에 파드를 조회해보면 끝난 파드들이 보인다.
기본적으로 init 컨테이너가 한번 실행되고, 이후에 실제 워크플로우의 진입점 역할을 하는 컨테이너가 워크플로우 간 각종 변수나 세팅 초기화 및 필요한 설정을 붙잡는 역할로 위치하는 것 같다.
그와 동시에 템플릿에 정의된 컨테이너가 실행된다.

ARGO_OS="darwin"
if [[ uname -s != "Darwin" ]]; then
  ARGO_OS="linux"
fi
curl -sLO "https://github.com/argoproj/argo-workflows/releases/download/v3.6.5/argo-$ARGO_OS-amd64.gz"
gunzip "argo-$ARGO_OS-amd64.gz"
chmod +x "argo-$ARGO_OS-amd64"
mv "./argo-$ARGO_OS-amd64" /usr/local/bin/argo
argo version

cli 툴을 이용해서 조금 더 간단하게 세팅하고 시각화를 이쁘게 해보자!
image.png
설치 자체야 간단하니 이제 조금 더 복잡한 워크플로우를 만들어 테스트해보자.
image.png
cli를 써보니까, 이거 안 쓰면 조금 힘들 수도 있겠다 싶다.
이걸로 미리 에러를 잡고 워크플로우를 실행할 수 있어서 꽤나 유용하긴 하다.

http 워크플로우

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: http-
spec:
  serviceAccountName: argo-workflow
  entrypoint: http
  onExit: http
  arguments:
    parameters:
    - name: url
      value: "http://nginx.default.svc"
  templates:
  - name: http
    inputs:
      parameters:
        - name: url
    http:
      timeoutSeconds: 20
      url: "{{inputs.parameters.url}}"
      method: "GET" # Default GET
      successCondition: "response.statusCode == 200" # available since v3.3

일단 감을 잡기 위해 조금 더 실습을 진행한다.
image.png
테스트를 해보니 http 템플릿의 경우 레거시 서비스어카운트 토큰을 활용하는 것이 보인다.
그러나 현 버전에서는 이 방식이 사라지고 projected 볼륨으로 짧은 생명 주기를 가진 서비스 어카운트 토큰이 TokenRequest 리소스를 통해 요청되어 붙는 방식으로 바뀌었기 때문에, 마운팅이 진행되지 못하고 펜딩이 걸린다.
버그라면 버그인데, 아마 곧 업데이트가 되지 않을까 싶다.

apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: argo-workflow.service-account-token
  annotations:
    kubernetes.io/service-account.name: argo-workflow

아쉬운 대로 직접 만들었다.
업데이트가 되기 이전까지, http 템플릿은 사용하지 않는 것이 좋겠다.
image.png
그러면 이런 식으로 일단 워크플로우가 진행은 된다.
image.png
조금 수정을 했는데, onExit 필드에 http 템플릿을 쓰면 무조건 context canceled가 뜨는 것 같다.
이건 이유를 잘 모르겠다. - 지금 다시 보니 그냥 내 물리 자원 스펙 문제였을 수도
대체로 봤을 때 종료 핸들러에는 컨테이너 템플릿을 넣어서 아티팩트에 데이터를 올리거나, 웹훅에 메시지를 날리는 식으로 하는 듯하다.
근데 반복적으로 워크플로우를 만들어보았을 때, 성공할 때도 있었고 아닐 때도 있었다.
이건 단순한 내 노드의 사양 문제일 수도 있겠다는 생각이 들었다.
image.png
전부 같은 양식의 워크플로우인데, 저마다 결과가 다르다..ㅋㅋ
중간 워크플로우는 처음부터 context canceled가 뜨고는 이후 단계를 진행하지도 않는다.

복잡한 워크플로우

여태 배운 것들을 토대로, 최대한 어렵게 어렵게 웤플을 만들어보자!
최종적으로 세팅해본 워크플로우는 다음과 같다.

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: complicated
spec:
  serviceAccountName: argo-workflow
  entrypoint: diamond
  arguments:
    parameters:
    - name: url
      value: "nginx.default.svc"
  volumeClaimTemplates:
  - metadata:
      name: complicated   
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Mi
  templates:
  - name: diamond
    dag:
      tasks:
      - name: A
        template: check-artifact
        arguments:
          artifacts:
          - name: git
            git:
              repo: https://gitea.zerotay.com/admin/develop.git
              revision: "main"
              usernameSecret:
                name: gitea-creds
                key: username
              passwordSecret:
                name: gitea-creds
                key: password
              singleBranch: true
              branch: main
      - name: B
        dependencies: [A]
        template: gen-random-int
        arguments:
          parameters: [{name: message, value: B}]
      - name: C
        dependencies: [B]
        template: print-output
        arguments:
          parameters:
          - name: message
            value: "{{tasks.B.outputs.result}}"
      - name: D
        dependencies: [A]
        template: cat-os-release
        arguments:
          parameters:
          - name: image
            value: "{{item.image}}"
          - name: tag
            value: "{{item.tag}}"
        withItems:
        - { image: 'debian', tag: '9.1' }       
        - { image: 'debian', tag: '8.9' }      
        - { image: 'alpine', tag: '3.6' }     
        - { image: 'ubuntu', tag: '17.10'}   
      - name: E
        dependencies: [D]
        template: print-volume
      - name: F
        dependencies: [C, E]
        template: echo
        arguments:
          parameters: [{name: message, value: F}]

  - name: check-artifact
    inputs:
      artifacts:
      - name: git
        path: /mnt/git
    container:
      image: alpine:latest
      command: [sh,-c]
      args: ["cp -r /mnt/git /mnt/vol/git"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: gen-random-int
    script:
      image: python:alpine3.6
      command: [python]
      source: |
        import random
        i = random.randint(1, 100)
        print(i)

  - name: print-output
    inputs:
      parameters:
      - name: message
    script:
      image: alpine:latest
      command: [sh, -c]
      source: |
        echo {{inputs.parameters.message}}

  - name: cat-os-release
    inputs:
      parameters:
      - name: image
      - name: tag
    container:
      image: "{{inputs.parameters.image}}:{{inputs.parameters.tag}}"
      command: [sh, -c]
      args: ["cat /etc/os-release >> /mnt/vol/test"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: print-volume
    container:
      image: alpine:latest
      command: [sh, -c]
      args: ["cat /mnt/vol/test"]
      volumeMounts:
      - name: complicated
        mountPath: /mnt/vol

  - name: echo
    inputs:
      parameters:
      - name: message
    container:
      image: alpine:3.7
      command: [echo, "{{inputs.parameters.message}}"]

문서에서 본 내용들을 거의 다 때려박아본 것이다.
아티팩트 세팅 중, 깃 관련 세팅을 어떻게 하는지 나오는 예시 파일을 찾아서 참고했다.[5]
문서에는 아티팩트 관련 내용이 자세히 나와있지 않지만, 예시 파일이 많아서 참고할 만하다.
image.png
엄.
image.png
오? 문서를 보면서 봤던 것들 일단 다 때려박아본 건데 생각보다 잘 된다.
원래 마지막에 http 템플릿을 사용했는데, 위의 실습에서 http 템플릿이 여러 문제가 있다는 것을 알게 돼서 빼버렸다.
image.png
요런 식으로, 세로로도 이쁘게 나온다!
뭔가 시각적으로 보이니까 알게 모르게 충족감이 있다 ㅋㅋ
image.png
ui로 생각보다 많은 내용들을 확인할 수 있다.
어차피 cli로도 다 할 수 있는 작업들이기는 하다.
혹시 데이터를 스토리지에 남길 수도 있나 싶어서 pvc를 써본 건데 워크플로우가 끝난 이후 pvc도 삭제됐다.
스토리지 클래스 회수 정책을 세팅하면 데이터야 남아있겠다만, 최소한 워크플로우에서는 그런 용도로 쓰라고 만든 게 아닌 건 확실하다.
image.png
파드는 이런 식으로 성공 상태로 남았다.
여러 노드에 배치된 것이 보이는데, 더 상세하게 파보지는 못했으나 실행할 노드를 지정해서 워크플로우 간 지역성을 높여 효율을 도모하는 것도 가능하다고 봤던 것 같다.

argo get 웤플이름

(아니 근데 자동완성 지원을 할 거면 워크플로우에 대해서 자동완성되게 하라고 ㅡㅡ)
image.png
이렇게 더 예쁘게도 볼 수 있다.

결론

워크플로우를 통해 컨테이너 환경을 적극 활용해 파이프라인을 구성할 수 있다는 것은 매우 좋은 운영 전략이 될 수 있다고 생각한다.
쓰면서 느끼는 건데, 다양한 설정을 할 수 있는 것도 좋고, 생각보다 그리 어렵지도 않다.

그러나 아직 많이 발전해야 할 것으로 보인다.
워크플로우를 잘 짜면 같은 동작을 해도 에러가 안 나게도 할 수 있긴 한데, 애초에 에러가 안 나와야 할 상황에서 에러를 내뿜는 것이 퍽 아쉽다는 것이 내 생각이다.
아니면 조금 더 명확한 가이드라인이라도 제공해주면 좋겠다.
복잡하고 긴 파이프라인을 간결하고 가볍게 실행할 수 있다는 점에서, 아르고 워크플로우는 충분히 활용성이 있다고 느꼈다.

다른 파이프라인 툴로서 Tekton이 있다고 들었는데, 언젠가 시간이 되면 비교 정리를 해보는 것도 좋을 것 같다.
(무슨 일이 있어도 젠킨스 다시는 안 쓰고 싶다는 강한 의지)

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 1 published 2025-02-03
2W - 테라폼으로 환경 구성 및 VPC 연결 2 published 2025-02-11
2W - EKS VPC CNI 분석 3 published 2025-02-11
2W - ALB Controller, External DNS 4 published 2025-02-15
3W - kubestr과 EBS CSI 드라이버 5 published 2025-02-21
3W - EFS 드라이버, 인스턴스 스토어 활용 6 published 2025-02-22
4W - 번외 AL2023 노드 초기화 커스텀 7 published 2025-02-25
4W - EKS 모니터링과 관측 가능성 8 published 2025-02-28
4W - 프로메테우스 스택을 통한 EKS 모니터링 9 published 2025-02-28
5W - HPA, KEDA를 활용한 파드 오토스케일링 10 published 2025-03-07
5W - Karpenter를 활용한 클러스터 오토스케일링 11 published 2025-03-07
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 12 published 2025-03-15
6W - api 구조와 보안 1 - 인증 13 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 14 published 2025-03-16
6W - EKS 파드에서 AWS 리소스 접근 제어 15 published 2025-03-16
6W - EKS api 서버 접근 보안 16 published 2025-03-16
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 17 published 2025-03-22
7W - EKS Fargate 18 published 2025-03-22
7W - EKS Automode 19 published 2025-03-22
8W - 아르고 워크플로우 20 published 2025-03-30
8W - 아르고 롤아웃 21 published 2025-03-30
8W - 아르고 CD 22 published 2025-03-30
8W - CICD 23 published 2025-03-30
9W - EKS 업그레이드 24 published 2025-04-02
10W - Vault를 활용한 CICD 보안 25 published 2025-04-16
11W - EKS에서 FSx, Inferentia 활용하기 26 published 2025-04-18
11주차 - EKS에서 FSx, Inferentia 활용하기 26 published 2025-05-11
12W - VPC Lattice 기반 gateway api 27 published 2025-04-27

관련 문서

이름 noteType created
Argo CD knowledge 2025-03-24
Argo Workflows knowledge 2025-03-24
Argo Rollouts knowledge 2025-03-24
아르고 롤아웃과 이스티오 연계 knowledge 2025-04-22
E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습 topic/explain 2025-03-30

참고


  1. https://argo-workflows.readthedocs.io/en/latest/architecture/ ↩︎

  2. https://argo-workflows.readthedocs.io/en/latest/walk-through/the-structure-of-workflow-specs/ ↩︎

  3. https://argo-workflows.readthedocs.io/en/latest/events/ ↩︎

  4. https://argo-workflows.readthedocs.io/en/stable/webhooks/ ↩︎

  5. https://github.com/argoproj/argo-workflows/blob/main/examples/input-artifact-git.yaml ↩︎